﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Threading;

namespace Heimdallr
{
    /// <summary>
    /// Represents a continuous web polling client. This is an abstract class.
    /// </summary>
    public abstract class WebPollingClientElement : ServiceElement, ITriggerElement
    {
        #region Field

        private readonly Uri pollingUri;
        private readonly BackgroundWorker worker;

        private TimeSpan duration;
        private int maxReceivedSize;
        private bool asyncResponse;

        #endregion

        #region Property

        int ITriggerElement.MaxReceivedSize
        {
            get { return this.maxReceivedSize; }
            set { this.maxReceivedSize = value; }
        }

        /// <summary>
        /// Gets the polling URI of remote server.
        /// </summary>
        public Uri PollingUri
        {
            get { return this.pollingUri; }
        }

        /// <summary>
        /// Gets or sets the duration between each polling action.
        /// The default value is <see cref="TimeSpan.Zero"/>.
        /// </summary>
        public TimeSpan Duration
        {
            get { return this.duration; }
            set { this.duration = value.Duration(); }
        }

        /// <summary>
        /// Gets or sets a value which indicates if uses asynchronized response mode or not. 
        /// </summary>
        public bool IsAsyncResponseEnabled
        {
            get { return this.asyncResponse; }
            set { this.asyncResponse = value; }
        }

        #endregion

        #region Event

        /// <summary>
        /// Occurs when a notification has been created.
        /// </summary> 
        public event FireNotificationEventHandler Notify;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of this class, using specified polling URI.
        /// </summary>
        /// <param name="pollingUri">The specified polling URI.</param>
        /// <exception cref="ArgumentNullException"><paramref name="pollingUri"/> is null.</exception>
        public WebPollingClientElement(Uri pollingUri)
            : this(pollingUri, TimeSpan.Zero)
        {
        }

        /// <summary>
        /// Initializes a new instance of this class, using specified polling URI and duration
        /// </summary>
        /// <param name="pollingUri">The specified polling URI.</param>
        /// <param name="duration">The duration between each polling action.</param>
        /// <exception cref="ArgumentNullException"><paramref name="pollingUri"/> is null.</exception>
        public WebPollingClientElement(Uri pollingUri, TimeSpan duration)
        {
            if (pollingUri == null)
                throw new ArgumentNullException("pollingUri");

            this.pollingUri = pollingUri;
            this.duration = duration.Duration();
            this.asyncResponse = false;

            this.worker = new BackgroundWorker();
            this.worker.WorkerSupportsCancellation = true;
            this.worker.DoWork += this.BackgroundWorker_DoWork;
            this.worker.RunWorkerCompleted += this.BackgroundWorker_RunWorkerCompleted;
        }

        #endregion

        #region ServiceElement Implementation

        /// <summary>
        /// Executes when the status of element is changing from <see cref="ServiceElementStatus">Stopped</see> to <see cref="ServiceElementStatus">Running</see>.
        /// </summary>
        protected override void OnStart()
        {
            this.worker.RunWorkerAsync();
        }

        /// <summary>
        /// Executes when the status of element is changing from <see cref="ServiceElementStatus">Running</see> to <see cref="ServiceElementStatus">Stopped</see>.
        /// </summary>
        protected override void OnStop()
        {
            this.worker.CancelAsync();
        }

        #endregion

        #region BackgroundWorker

        private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            while (!this.worker.CancellationPending)
            {
                WebRequest request = null;

                try
                {
                    request = WebRequest.Create(pollingUri);
                }
                catch (Exception error)
                {
                    throw new Exception(string.Format("An error has occurred when creates a WebRequest instance of {0}.", pollingUri), error);
                }

                object userState = this.BeforSendRequest(this.pollingUri, ref request);

                if (request == null)
                {
                    Thread.Sleep(this.duration);
                    continue;
                }

                if (this.asyncResponse)
                {
                    try
                    {
                        request.BeginGetResponse(this.GetResponseCallback, Tuple.Create(request, userState));
                    }
                    catch (Exception error)
                    {
                        this.RaiseNotify(this.AfterGetResponse(request, null, error, userState));
                    }
                }
                else
                {
                    WebResponse response = null;

                    try
                    {
                        response = request.GetResponse();
                    }
                    catch (Exception error)
                    {
                        this.RaiseNotify(this.AfterGetResponse(request, null, error, userState));
                        continue;
                    }
                    this.RaiseNotify(this.AfterGetResponse(request, response, null, userState));
                }
                Thread.Sleep(this.duration);
            }
        }

        private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
                this.OnError(new ExceptionsEventArgs(e.Error));
        }

        #endregion

        #region Protected Method

        /// <summary>
        /// When implemented in a derived class, executes before a request is sent to remote server.
        /// </summary>
        /// <param name="pollingUri">The specified polling URI.</param>
        /// <param name="request">The request which is ready to be sent.</param>
        /// <returns>
        /// The object that is returned as the userState argument of the <see cref="AfterGetResponse"/> method. 
        /// This is null if no correlation state is used.
        /// </returns>
        protected virtual object BeforSendRequest(Uri pollingUri, ref WebRequest request)
        {
            return null;
        }

        /// <summary>
        /// When implemented in a derived class, executes after getting response from remote server. 
        /// </summary>
        /// <param name="request">The request which has been sent.</param>
        /// <param name="response">The response from remote server.</param>
        /// <param name="error">The error that occurs while getting response from remote server.</param>
        /// <param name="userState">The returned value of <see cref="BeforSendRequest"/> method.</param>
        /// <returns>The notifications which are generated by the reponse of remote server.</returns>
        protected abstract IEnumerable<Notification> AfterGetResponse(WebRequest request, WebResponse response, Exception error, object userState);

        #endregion

        #region Callback

        private void GetResponseCallback(IAsyncResult r)
        {
            Tuple<WebRequest, object> tuple = r.AsyncState as Tuple<WebRequest, object>;
            WebRequest request = tuple.Item1;
            object userState = tuple.Item2;

            WebResponse response = null;

            try
            {
                response = request.EndGetResponse(r);
            }
            catch (Exception error)
            {
                this.RaiseNotify(this.AfterGetResponse(request, null, error, userState));
                return;
            }

            using (response)
                this.RaiseNotify(this.AfterGetResponse(request, response, null, userState));
        }

        #endregion

        #region Raise Event

        private void RaiseNotify(IEnumerable<Notification> notifications)
        {
            if (notifications == null)
                return;

            var temp = from n in notifications
                       where n != null
                       select new FireNotificationEventArgs(n);

            foreach (var e in temp)
                this.OnNotify(e);
        }

        /// <summary>
        /// Raises the <see cref="Notify"/> event.
        /// </summary>
        /// <param name="e">Event data for the event.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="e"/> is null.</exception>
        protected virtual void OnNotify(FireNotificationEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            if (this.Notify != null)
                this.Notify.BeginInvoke(this, e, this.Notify.EndInvoke, null);
        }

        #endregion
    }
}
